Skip to content

feat: support locally provided CLI binaries via binaryDestination#286

Merged
fioan89 merged 14 commits intomainfrom
impl-support-for-local-cli
Apr 2, 2026
Merged

feat: support locally provided CLI binaries via binaryDestination#286
fioan89 merged 14 commits intomainfrom
impl-support-for-local-cli

Conversation

@fioan89
Copy link
Copy Markdown
Collaborator

@fioan89 fioan89 commented Mar 23, 2026

The existing CLI resolution logic hardcodes the CLI name based on the OS and architecture without any flexibility in specifying an existing CLI. It has a couple of options to specify the download dir (binDir and dataDir) but not the CLI name. This PR revolves around the bindDir setting which is now renamed to binaryDestination and it allows users to specify an absolute path to an existing executable filename or a destination dir. If the user specifies the destination dir then the CLI name will use the existing default name based on the os and architecture.

In addition a couple of windows tests were refactored because they required git bash utilities to be installed on Windows. Recent builds on the Github CI failed because commands like printf were not available.
A lot of UTs were also added to cover the behavior of CLI resolution when based on how binaryDestination, dataDir, enableDownloads and enableFallback flags are configured. Now there is also a special section in the README detailing the CLI resolution algorithm.

By and large we tried to keep the existing behavior without breaking compatibility for existing users who were supposed to configure a folder in the binaryDir.

The new behavior works like this:

1. Look at the binaryDestination, and if it points to an existing executable file check it if it's version matches server version:
    1.1 If it matches then return it.
    1.2 If it doesn't match then check whether downloads are enabled
        1.2.1 if it is enabled then download the server version of CLI and overwrite the one at binaryDestination
        1.2.2 if it is not enabled then check `$dataDir/$hostname/coder-$os-$arch.$ext
               1.2.2.1 if the CLI in the dataDir exists and matches, then return it.
                1.2.2.1 otherwise prefer the CLI from binaryDestination over the one from dataDir
2. If binaryDestination points to a directory, check $binDestination/$hostname/coder-$os-$arch.$ext if it's version matches server version:
    2.1 If it matches then return it.
    2.2 If it doesn't match then check whether downloads are enabled
        2.2.1 if it is enabled then download the server version of CLI and overwrite the one at $binDestination/$hostname/coder-$os-$arch.$ext 
        2.2.2 if it is not enabled then check `$dataDir/$hostname/coder-$os-$arch.$ext
               2.2.2.1 if the CLI in the dataDir exists and matches, then return it.
                2.2.2.1 otherwise prefer the CLI from $binDestination/$hostname/coder-$os-$arch.$ext  over the one from dataDir

fioan89 added 3 commits March 23, 2026 23:50
Reimplemented `binPath` to support three modes:
- default data directory
- absolute path to a pre-existing local CLI (when downloads are disabled)
- and base directory with host-specific subdirectory (when downloads are enabled)

This modes are controlled by the Binary directory setting now renamed to
Binary destination and the enable downloads setting.

- resolves #285
That tests the fallback configuration from binaryDestination
to the deprecated binaryDirectory.
In addition, the existing tests were updated as they now have to
take into account that enableDownloads option affects the output
of binPath API.
@fioan89 fioan89 requested review from code-asher and matifali March 24, 2026 20:26
Windows needs special handling because we can't mock
the exe binary as a bash script like we do on Linux/Mac.
In addition, there were a couple of unix paths hardcoded
in some of the tests.
fioan89 added 7 commits March 25, 2026 22:04
Windows needs special handling because we can't mock
the exe binary as a bash script like we do on Linux/Mac.
In addition, there were a couple of unix paths hardcoded
in some of the tests.
binaryDestination can now take a path to an executable or a path to a download directory.
In order to reflect the new behavior regarding CLI resolution in
general and the binary destination in particular.
Adds a chapter describing the behavior of binaryDestination,
dataDir, enableDownloads, and enableBinaryDirFallback, and
explains how these settings interact and work together.
Refactored some of the existing tests into a new
battery with a bunch additional tests to cover the complex
and intertwined way of working for binaryDestination, enableDownloads,
dataDir and enableFallback settings in the way CLI is resolved.
@matifali matifali removed their request for review March 28, 2026 20:54
Without GIT bash utilities installed. To remove the dependency
I duplicated some of the tests and used Windows commands only.
@fioan89 fioan89 requested review from EhabY and code-asher March 30, 2026 20:25
@EhabY
Copy link
Copy Markdown

EhabY commented Mar 31, 2026

I'm a bit confused by the dataDir / binaryDestination interplay here. To me (and this is how VS Code has been doing it for a while), if binaryDestination is set, it's the single source of truth. We never look at dataDir for the binary. The binary goes to <binaryDestination>/<cliName>. We also fall back to the CODER_BINARY_DESTINATION env var if the setting isn't configured. If neither is set, we default to <globalStoragePath>/<hostname>/bin.

That's the whole thing. No forceDownloadToData, no conditional fallback between two directories. One setting, one location. The implementation is about 8 lines: pathResolver.ts#L34-L41

I think trying to have binaryDestination sometimes look at dataDir as a fallback is where the complexity explodes. The behavior ends up depending on the combination of enableDownloads, enableBinaryDirectoryFallback, and forceDownloadToData, which is really hard to reason about. I'd lean toward: if binaryDestination is set, that's it, dataDir is out of the picture for the binary

@fioan89
Copy link
Copy Markdown
Collaborator Author

fioan89 commented Mar 31, 2026

I'm a bit confused by the dataDir / binaryDestination interplay here. To me (and this is how VS Code has been doing it for a while), if binaryDestination is set, it's the single source of truth. We never look at dataDir for the binary. The binary goes to <binaryDestination>/<cliName>. We also fall back to the CODER_BINARY_DESTINATION env var if the setting isn't configured. If neither is set, we default to <globalStoragePath>/<hostname>/bin.

That's the whole thing. No forceDownloadToData, no conditional fallback between two directories. One setting, one location. The implementation is about 8 lines: pathResolver.ts#L34-L41

I think trying to have binaryDestination sometimes look at dataDir as a fallback is where the complexity explodes. The behavior ends up depending on the combination of enableDownloads, enableBinaryDirectoryFallback, and forceDownloadToData, which is really hard to reason about. I'd lean toward: if binaryDestination is set, that's it, dataDir is out of the picture for the binary

You are absolutely right. The complexity explodes due to so many controlling flags and options and combinations of them. Now I don't have the full context but it seems like all of these were dragged from the Gateway support. I'll have to dig up the git history and understand the context around why all of them were needed.

Now to your point I think dataDir is the equivalent of globalStoragePath It is a configurable directory with a default location. Of course flags like enable fallback and enable downloads only makes things worse. I would challenge all of this but as @code-asher mentioned, we do need to pay attention to existing customers who depend on these settings.

@code-asher
Copy link
Copy Markdown
Member

code-asher commented Mar 31, 2026

I think the data dir fallback was not explicitly requested by any customer.

Someone asked for the ability to customize the binary path to a location only the sysadmin could write to, and the setting would also be controlled by the admin, and I think the logic was "well if that binary is bad or missing or out of date we want to be able to still fall back and download a working binary".

But I am not sure anyone relies on that behavior. One problem is, if sysadmins are doing this, usually it is because unauthorized binaries are not allowed to run on the system anyway. So this fallback would not work in any case. Possibly we can safely remove it without affecting anyone.

But yeah we still need dataDir as an equivalent to globalStoragePath, but maybe we can skip the fallback part. idk, we would have to look into it

@fioan89
Copy link
Copy Markdown
Collaborator Author

fioan89 commented Mar 31, 2026

There is no support in Toolbox but I think we can write our own migration support for these settings. So that should ease out some of the concerns if it ever gets to the point where we decide to simplify. More or less to me it looks like VS Code extension still relies on two paths just that it cascades automatically without the complication of "enable fallback" which BTW is restricted ONLY to access denied errors.

@fioan89 fioan89 requested a review from EhabY March 31, 2026 18:51
@EhabY
Copy link
Copy Markdown

EhabY commented Apr 1, 2026

VS Code extension still relies on two paths just that it cascades automatically without the complication of "enable fallback" which BTW is restricted ONLY to access denied errors.

Yes that is my only concern tbh, if binaryDestination is explicitly configured then an accessibility/permission failure should surface to the user rather than silently falling back to dataDir. Otherwise the user has no idea their configured path isn't actually being used. Like if the user explicitly set binaryDestination, respect that intent and fail loudly when it can't be used rather than quietly switching to a different location behind the scenes. (even if there's an explicit setting for it)

Two small things I noticed:

  • The settings page still saves via updateBinaryDirectory() which writes to the old BINARY_DIRECTORY key, so BINARY_DESTINATION is never actually populated through the UI even though it's marked as the replacement. Is this intentional as a backwards-compat bridge or just missed during the rename?
  • binaryName is fully removed with no fallback (unlike binaryDirectory which has the ?: store[BINARY_DIRECTORY] fallback). Was this setting actually used by anyone?

Everything else looks good to me and I like the much improved version here! Test coverage is solid and the docs are clear!

@fioan89
Copy link
Copy Markdown
Collaborator Author

fioan89 commented Apr 1, 2026

if binaryDestination is explicitly configured then an accessibility/permission failure should surface to the user rather than silently falling back to dataDir.

This is what happens for any other type of issue. But not in the case access denied exceptions. I'll talk to Atif and see if can get greenlight to simplify this. I tracked down the original change and Asher was right, it was not requested by anyone.
coder/jetbrains-coder#246

The settings page still saves via updateBinaryDirectory() which writes to the old BINARY_DIRECTORY key
It was intentional in original version, but after the rewrite discussed with Asher it became un-intentional. Thx for pointing that out.

binaryName is fully removed with no fallback
It was never configurable from UI (only if someone tampered manually with the settings.json). But at least in Toolbox world, that is not a recommended practice. For example if you screw the json schema by misplacing a "," then Toolbox will truncate the file because it is not lenient in parsing the json.

@code-asher
Copy link
Copy Markdown
Member

This is what happens for any other type of issue. But not in the case access denied exceptions. I'll talk to Atif and see if can get greenlight to simplify this. I tracked down the original change and Asher was right, it was not requested by anyone.
coder/jetbrains-coder#246

lol I seem to have predicted the future in that PR:

Hopefully I have not just overcomplicated things though...

Comment on lines +457 to +459
- `Enable binary directory fallback` when enabled, if the binary destination is not writable the
plugin falls back to the data directory instead of failing. Only takes effect when downloads are
enabled and the binary destination differs from the data directory. Defaults to disabled.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an observation, but if we end up having to keep support for the fallback, then we might consider making this option also affect when downloads are disabled, so admins have the ability to always force using the configured binary even if a user places their own more up-to-date binary in their data directory.

If we can remove the fallback though then this disappears anyway.

And use the correct key, instead of the deprecated one.
@EhabY
Copy link
Copy Markdown

EhabY commented Apr 1, 2026

Not a blocker but would it make sense that if the binaryDestination is a folder we check coder[.exe] and coder-${os}-${arch}[.exe]? Because I think the binary we download automatically uses the long name but if you manually go to the releases and download the one you want it's just plain coder

Of course people can just point to the binary and that would work but yeah

@fioan89
Copy link
Copy Markdown
Collaborator Author

fioan89 commented Apr 1, 2026

Not a blocker but would it make sense that if the binaryDestination is a folder we check coder[.exe] and coder-${os}-${arch}[.exe]? Because I think the binary we download automatically uses the long name but if you manually go to the releases and download the one you want it's just plain coder

Of course people can just point to the binary and that would work but yeah

I'm simply afraid of the danger of overwriting a manually downloaded CLI when the version no longer matches. Maybe instead we could check (in the order of priority):

  • $binaryDestination/$hostname/coder-$os-$arch[.exe]
  • $binaryDestination/$hostname/coder[.exe]

But then it feels like once again we overengineer the CLI resolution 🤔

@code-asher
Copy link
Copy Markdown
Member

code-asher commented Apr 2, 2026

But then it feels like once again we overengineer the CLI resolution

Def agree with this. I do still kind of wish we could make a brand new setting with placeholders (defaults to $dataDir/$hostname/coder-$os-$arch) then we could support any path without any ambiguity because it would always point to a file.

@fioan89 fioan89 merged commit 015ac13 into main Apr 2, 2026
6 checks passed
@fioan89 fioan89 deleted the impl-support-for-local-cli branch April 2, 2026 18:39
fioan89 added a commit that referenced this pull request Apr 2, 2026
This PR is a result of discussion that happened in the
#286.
Basically it as an attempt of simplifying the CLI resolution
and trying to be more aligned with the VS Code extension.
The existing implementation was too cumbersome to understand, brittle
and a lot of tedious work needed to solve all of it's usecases.

This PR removes the enable fallback to data dir setting, which was used
only for access denied exceptions. The CLI resolution uses the binary destination
if it was configured by the user, or it automatically falls back to data dir
if binary destination was not configured. The implementation respects the user's
choice and no longer tries to make smart choices on behalf of the user. For example
if the user configured the binary destination but disabled downloads we just prompt
him with an error instead of trying to fall on data dir.

There were a couple of other small improvements left from the previous PR that are
now addressed.

- resolves #285
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

If coder is installed via winget, local binary fallback fails because plugin expects release artifact name instead of installed executable name

3 participants